/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.Text;
using Bitcoin.Cryptography;
using Bitcoin.Diagnostics;
using Bitcoin.IO;
using Org.BouncyCastle.Utilities.Encoders;

namespace Bitcoin
{
    /// <summary>
    ///     A transaction represents the movement of coins from some addresses to some other addresses. It can also represent
    ///     the minting of new coins. A Transaction object corresponds to the equivalent in the BitCoin C++ implementation.
    /// </summary>
    /// <remarks>
    ///     It implements TWO serialization protocols - the BitCoin proprietary format which is identical to the C++
    ///     implementation and is used for reading/writing transactions to the wire and for hashing. It also implements Java
    ///     serialization which is used for the wallet. This allows us to easily add extra fields used for our own accounting
    ///     or UI purposes.
    /// </remarks>
    //[Serializable]
    public class Transaction : Message
    {
        /// <summary>
        ///     These constants are a part of a scriptSig signature on the inputs. They define the details of how a
        ///     transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.
        /// </summary>
        /// <remarks>
        ///     In the official client, this enum also has another flag, SIGHASH_ANYONECANPAY. In this implementation,
        ///     that's kept separate. Only SIGHASH_ALL is actually used in the official client today. The other flags
        ///     exist to allow for distributed contracts.
        /// </remarks>
        public enum SigHash
        {
            All, // 1
            None, // 2
            Single, // 3
        }

        private ILog _log = LogService.GetLog(typeof(Transaction));
        private Sha256Hash _hash;

        // These are serialized in both BitCoin and java serialization.
        private List<TransactionInput> _inputs;
        private uint _lockTime;
        private List<TransactionOutput> _outputs;
        private uint _version;

        // This is an in memory helper only.
        /*[NonSerialized]*/

        public Transaction(UInt32 version, IEnumerable<TransactionInput> inputs, IEnumerable<TransactionOutput> outputs,
            UInt32 lockTime)
        {
            _version = version;
            _inputs = new List<TransactionInput>(inputs);
            _outputs = new List<TransactionOutput>(outputs);
            _lockTime = lockTime;
        }

        public Transaction(ChainParameters parameters)
            : base(parameters)
        {
            _version = 1;
            _inputs = new List<TransactionInput>();
            _outputs = new List<TransactionOutput>();
            // We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet.
        }

        /// <summary>
        ///     Creates a transaction from the given serialized bytes, eg, from a block or a tx network message.
        /// </summary>
        /// <exception cref="ProtocolException" />
        public Transaction(ChainParameters parameters, byte[] payloadBytes)
            : base(parameters, payloadBytes, 0)
        {
        }

        /// <summary>
        ///     Creates a transaction by reading payload starting from offset bytes in. Length of a transaction is fixed.
        /// </summary>
        /// <exception cref="ProtocolException" />
        public Transaction(ChainParameters parameters, byte[] payload, int offset)
            : base(parameters, payload, offset)
        {
            // inputs/outputs will be created in parse()
        }


        /// <summary>
        ///     Returns a set of blocks which contain the transaction, or null if this transaction doesn't have that data
        ///     because it's not stored in the wallet or because it has never appeared in a block.
        /// </summary>
        public ICollection<StoredBlock> AppearsIn { get; private set; }

        /// <summary>
        ///     Returns the transaction hash as you see them in the block explorer.
        /// </summary>
        public Sha256Hash Hash
        {
            get
            {
                if (_hash == null)
                {
                    byte[] bytes;
                    using (var s = new MemoryStream())
                    using (var w = new BinaryWriter(s))
                    {
                        w.Write(this);
                        bytes = s.ToArray();
                    }
                    _hash = new Sha256Hash(SecurityUtility.DoubleDigest(bytes));
                }
                return _hash;
            }
        }

        public string HashAsString
        {
            get { return Hash.ToString(); }
        }

        /// <summary>
        ///     Returns a read-only list of the inputs of this transaction.
        /// </summary>
        public IList<TransactionInput> Inputs
        {
            //get { return _inputs.AsReadOnly(); }
            get { return _inputs; }
        }

        /// <summary>
        ///     A coinbase transaction is one that creates a new coin. They are the first transaction in each block and their
        ///     value is determined by a formula that all implementations of BitCoin share. In 2011 the value of a coinbase
        ///     transaction is 50 coins, but in future it will be less. A coinbase transaction is defined not only by its
        ///     position in a block but by the data in the inputs.
        /// </summary>
        public bool IsCoinBase
        {
            get { return _inputs[0].IsCoinBase; }
        }

        public uint LockTime
        {
            get { return _lockTime; }
        }

        public ILog Log
        {
            get { return _log; }
            set { _log = value; }
        }

        /// <summary>
        ///     Returns a read-only list of the outputs of this transaction.
        /// </summary>
        public IList<TransactionOutput> Outputs
        {
            //get { return _outputs.AsReadOnly(); }
            get { return _outputs; }
        }

        public uint Version
        {
            get { return _version; }
        }

        /// <summary>
        ///     Adds the given block to the internal serializable set of blocks in which this transaction appears. This is
        ///     used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
        ///     block stores do not save the transaction data at all.
        /// </summary>
        public void AddBlockAppearance(StoredBlock block)
        {
            if (AppearsIn == null)
            {
                AppearsIn = new HashSet<StoredBlock>();
            }
            AppearsIn.Add(block);
        }

        /// <summary>
        ///     Adds an input to this transaction that imports value from the given output. Note that this input is NOT
        ///     complete and after every input is added with addInput() and every output is added with addOutput(),
        ///     signInputs() must be called to finalize the transaction and finish the inputs off. Otherwise it won't be
        ///     accepted by the network.
        /// </summary>
        public void AddInput(TransactionOutput from)
        {
            AddInput(new TransactionInput(Params, this, from));
        }

        /// <summary>
        ///     Adds an input directly, with no checking that it's valid.
        /// </summary>
        public void AddInput(TransactionInput input)
        {
            _inputs.Add(input);
        }

        /// <summary>
        ///     Adds the given output to this transaction. The output must be completely initialized.
        /// </summary>
        public void AddOutput(TransactionOutput to)
        {
            to.ParentTransaction = this;
            _outputs.Add(to);
        }

        /// <exception cref="IOException" />
        [Obsolete("Use BitcoinBinaryWriterExtensions")]
        public override void BitcoinSerializeToStream(Stream stream)
        {
            // TODO: Remove serialization altogether from Message (should be decoupled)

            using (var s = new MemoryStream())
            using (var w = new BinaryWriter(s))
            {
                w.Write(this);
                s.WriteTo(stream);
            }
        }

        /// <summary>
        ///     Connects all inputs using the provided transactions. If any input cannot be connected returns that input or
        ///     null on success.
        /// </summary>
        public TransactionInput ConnectForReorganize(IDictionary<Sha256Hash, Transaction> transactions)
        {
            foreach (var input in _inputs)
            {
                // Coinbase transactions, by definition, do not have connectable inputs.
                if (input.IsCoinBase) continue;
                var result = input.Connect(transactions, false);
                // Connected to another tx in the wallet?
                if (result == TransactionInput.ConnectionResult.Success)
                    continue;
                // The input doesn't exist in the wallet, eg because it belongs to somebody else (inbound spend).
                if (result == TransactionInput.ConnectionResult.NoSuchTx)
                    continue;
                // Could not connect this input, so return it and abort.
                return input;
            }
            return null;
        }

        public bool DisconnectInputs()
        {
            var disconnected = false;
            foreach (var input in _inputs)
            {
                disconnected |= input.Disconnect();
            }
            return disconnected;
        }

        public override bool Equals(object other)
        {
            if (!(other is Transaction)) return false;
            var t = (Transaction) other;

            return t.Hash.Equals(Hash);
        }

        public override int GetHashCode()
        {
            return Hash.GetHashCode();
        }

        public byte[] HashTransactionForSignature(SigHash type, bool anyoneCanPay)
        {
            byte[] bytes;
            using (var s = new MemoryStream())
            using (var w = new BinaryWriter(s))
            {
                w.Write(this);
                // We also have to write a hash type.
                var hashType = (uint) type + 1;
                if (anyoneCanPay)
                {
                    hashType |= 0x80;
                }
                w.Write(hashType);
                bytes = s.ToArray();
            }
            // Note that this is NOT reversed to ensure it will be signed correctly. If it were to be printed out
            // however then we would expect that it is IS reversed.
            return SecurityUtility.DoubleDigest(bytes);
        }

        /// <returns>true if every output is marked as spent.</returns>
        public bool IsEveryOutputSpent()
        {
            foreach (var output in _outputs)
            {
                if (output.IsAvailableForSpending)
                    return false;
            }
            return true;
        }

        /// <exception cref="ProtocolException" />
        protected override void Parse()
        {
            // TODO: Remove serialization altogether from Message (should be decoupled)
            //       i.e. components derive from MessagePayload, not message

            using (var s = new MemoryStream(Bytes))
            using (var reader = new BinaryReader(s))
            {
                s.Position = Cursor;

                _version = reader.ReadUInt32();

                var inputCount = reader.ReadVarIntEncoded();
                _inputs = new List<TransactionInput>((int)inputCount);
                for (var inputIndex = 0UL; inputIndex < inputCount; inputIndex++)
                {
                    var input = reader.ReadTransactionInput();
                    input.ParentTransaction = this;
                    _inputs.Add(input);
                }

                var outputCount = reader.ReadVarIntEncoded();
                _outputs = new List<TransactionOutput>((int)outputCount);
                for (var outputIndex = 0UL; outputIndex < outputCount; outputIndex++)
                {
                    var output = reader.ReadTransactionOutput();
                    output.ParentTransaction = this;
                    _outputs.Add(output);
                }

                _lockTime = reader.ReadUInt32();

                Cursor = (int)s.Position;
            }
        }

        /// <returns>A human readable version of the transaction useful for debugging.</returns>
        public override string ToString()
        {
            var s = new StringBuilder();
            s.Append("  ");
            s.Append(HashAsString);
            s.AppendLine();
            if (IsCoinBase)
            {
                string script;
                string script2;
                try
                {
                    script = _inputs[0].ScriptSig.ToString();
                    script2 = _outputs[0].ScriptPubKey.ToString();
                }
                catch (ScriptException)
                {
                    script = "???";
                    script2 = "???";
                }
                return "     == COINBASE TXN (scriptSig " + script + ")  (scriptPubKey " + script2 + ")";
            }
            foreach (var input in _inputs)
            {
                s.Append("     ");
                s.Append("from ");

                try
                {
                    //s.Append(input.ScriptSig.FromAddress.ToString());
                    var fromPubKey = input.ScriptSig.PubKey;
                    var fromHash = SecurityUtility.Sha256Hash160(fromPubKey);
                    s.Append(Hex.ToHexString(fromHash));
                }
                catch (Exception e)
                {
                    s.Append("[exception: ").Append(e.Message).Append("]");
                    throw;
                }
                s.AppendLine();
            }
            foreach (var output in _outputs)
            {
                s.Append("       ");
                s.Append("to ");
                try
                {
                    var toAddr = new BitcoinAddress(Params, output.ScriptPubKey.PubKeyHash);
                    s.Append(toAddr.ToString());
                    s.Append(" ");
                    s.Append(Temp_BitcoinUtility.BitcoinValueToFriendlyString(output.Value));
                    s.Append(" BTC");
                }
                catch (Exception e)
                {
                    s.Append("[exception: ").Append(e.Message).Append("]");
                }
                s.AppendLine();
            }
            return s.ToString();
        }

        public bool Verify(ValidationState state)
        {
            // Basic checks that don't depend on any context
            if (Inputs.Count == 0)
            {
                _log.Warn("CheckTransaction() : vin empty");
                state.AddDoS(10, ValidationState.REJECT_INVALID, "bad-txns-vin-empty", false);
                return false;
            }
            if (Outputs.Count == 0)
            {
                _log.Warn("CheckTransaction() : vout empty");
                state.AddDoS(10, ValidationState.REJECT_INVALID, "bad-txns-vout-empty", false);
                return false;
            }

            var serialized = BitcoinSerialize();
            if (serialized.Length > Block.MAX_BLOCK_SIZE)
            {
                _log.Warn("CheckTransaction() : size limits failed");
                state.AddDoS(100, ValidationState.REJECT_INVALID, "bad-txns-oversize", false);
                return false;
            }

            // Check for negative or overflow output values
            long totalOut = 0;
            foreach (var txout in Outputs)
            {
                if (txout.Value < 0)
                {
                    _log.Warn("CheckTransaction() : txout.nValue negative");
                    state.AddDoS(100, ValidationState.REJECT_INVALID, "bad-txns-vout-negative", false);
                    return false;
                }
                if (txout.Value > Temp_BitcoinUtility.MAX_MONEY)
                {
                    _log.Warn("CheckTransaction() : txout.nValue too high");
                    state.AddDoS(100, ValidationState.REJECT_INVALID, "bad-txns-vout-toolarge", false);
                    return false;
                }
                totalOut += (long) txout.Value;
                if (totalOut < 0 || totalOut > Temp_BitcoinUtility.MAX_MONEY)
                {
                    _log.Warn("CheckTransaction() : txout total out of range");
                    state.AddDoS(100, ValidationState.REJECT_INVALID, "bad-txns-txouttotal-toolarge", false);
                    return false;
                }
            }

            // Check for duplicate inputs
            var inOutPoints = new List<TransactionOutPoint>();
            foreach (var txin in Inputs)
            {
                if (inOutPoints.Contains(txin.Outpoint))
                {
                    _log.Warn("CheckTransaction() : duplicate inputs");
                    state.AddDoS(100, ValidationState.REJECT_INVALID, "bad-txns-inputs-duplicate", false);
                    return false;
                }
                inOutPoints.Add(txin.Outpoint);
            }

            if (IsCoinBase)
            {
                var script = Inputs[0].ScriptBytes;
                if (script.Length < 2 || script.Length > 100)
                {
                    _log.Warn("CheckTransaction() : coinbase script size");
                    state.AddDoS(100, ValidationState.REJECT_INVALID, "bad-cb-length", false);
                    return false;
                }
            }
            else
            {
                foreach (var txin in Inputs)
                {
                    if (txin.Outpoint == null)
                    {
                        _log.Warn("CheckTransaction() : prevout is null");
                        state.AddDoS(10, ValidationState.REJECT_INVALID, "bad-txns-prevout-null", false);
                        return false;
                    }
                }
            }

            return true;
        }
    }
}